Practical Python Design Patterns

What is a class in Python?


In [ ]:


In [1]:
class Test:
    pass

In [2]:
a = Test()

In [3]:
a


Out[3]:
<__main__.Test at 0x11115a048>

In [4]:
type(a)


Out[4]:
__main__.Test

In [5]:
type(Test)


Out[5]:
type

In [7]:
Test


Out[7]:
__main__.Test

What is type in Python?


In [6]:
type('TestWithType', (object,), {})


Out[6]:
__main__.TestWithType

In [8]:
type?

In [9]:
test_with_type = type('TestWithType', (object,), {})

In [10]:
a1 = test_with_type()

In [11]:
a1


Out[11]:
<__main__.TestWithType at 0x111173198>

In [12]:
dir(a1)


Out[12]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']

In [13]:
type('TestWithType', (object,), {})()


Out[13]:
<__main__.TestWithType at 0x111173208>

Life Cycle involved in a class


In [14]:
class TestClass:

    def __new__(cls, *args, **kwargs):
        print('new method called')
        instance = super(TestClass, cls).__new__(cls, *args, **kwargs)
        return instance

    def __call__(self, a, b, c):
        print('call method called')
        return a * b * c

    def __init__(self):
        super(TestClass, self).__init__()
        print('init method called')

In [15]:
a = TestClass()


new method called
init method called

In [16]:
a(1,2,3)


call method called
Out[16]:
6

Decorators


In [17]:
def func1(func2):
    def wrapper():
        return func2().lower()
    return wrapper

In [18]:
@func1
def get_value():
    return 'aBcDeFgH'

In [19]:
get_value()


Out[19]:
'abcdefgh'

In [20]:
func1(get_value)()


Out[20]:
'abcdefgh'

In [21]:
class MyDecorator:    
    def __init__(self, case):
        self.is_lower_case = False if case == 'upper' else True
        
    def __call__(self, func2):
        def wrapper():
            return func2().lower() if self.is_lower_case else func2().upper()
        return wrapper

In [25]:
user_input = 'upper'

In [26]:
@MyDecorator(user_input)
def get_value():
    return 'aBcDeFgH'

In [27]:
get_value()


Out[27]:
'ABCDEFGH'

In [28]:
from flask import Flask
app = Flask(__name__)

@app.route('/hw')
def hello_world():
    return 'Hello World!'

app.run()


 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [17/Mar/2018 11:44:46] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [17/Mar/2018 11:44:46] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [17/Mar/2018 11:44:51] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [17/Mar/2018 11:44:51] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [17/Mar/2018 11:44:54] "GET //hw HTTP/1.1" 404 -
127.0.0.1 - - [17/Mar/2018 11:44:59] "GET //hw HTTP/1.1" 404 -
127.0.0.1 - - [17/Mar/2018 11:45:03] "GET /hw HTTP/1.1" 200 -

Back to Meta classes

What is type? 'type' defines how a class behaves in Python.

Got it. Well then - Can I change 'how' a class behaves in Python? - MetaClasses


In [29]:
class MySingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MySingletonMeta, cls).__call__(*args)
        return cls._instances[cls]

In [30]:
class MySingletonClass(metaclass=MySingletonMeta):
    def __init__(self):
        self.i = 1

In [31]:
a = MySingletonClass()
b = MySingletonClass()

In [32]:
a is b


Out[32]:
True

In [33]:
type(a), id(a) , type(b), id(b)


Out[33]:
(__main__.MySingletonClass, 4588486608, __main__.MySingletonClass, 4588486608)

In [34]:
from abc import ABCMeta, ABC, abstractmethod

In [35]:
ABCMeta?

In [40]:
class MyAbstractClass(metaclass=ABCMeta):
    def __init__(self):
        pass

    @abstractmethod
    def my_abstract_method(self):
        pass

In [41]:
MyAbstractClass()


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-41-b0c6258176f1> in <module>()
----> 1 MyAbstractClass()

TypeError: Can't instantiate abstract class MyAbstractClass with abstract methods my_abstract_method

In [46]:
class MyChildClass(MyAbstractClass):
    
    def __init__(self):
        pass
    
    def my_abstract_method(self):
        pass
    
    @staticmethod
    def my_static_method():
        print('I am a static method')
        
    @classmethod
    def my_class_method(cls):
        print('Class method called')

In [47]:
mcc = MyChildClass()

In [48]:
mcc.my_static_method()


I am a static method

In [49]:
MyChildClass.my_class_method()


Class method called

Combine two meta classes


In [50]:
class MySingletonABCMeta(ABCMeta):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MySingletonABCMeta, cls).__call__(*args)
        return cls._instances[cls]

In [51]:
class MyAbstractSingletonClass(metaclass=MySingletonABCMeta):
    def __init__(self):
        pass

    @abstractmethod
    def my_abstract_method(self):
        pass

In [52]:
MyAbstractSingletonClass()


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-bf201b785df9> in <module>()
----> 1 MyAbstractSingletonClass()

<ipython-input-50-b2bf85fdf0ae> in __call__(cls, *args, **kwargs)
      4     def __call__(cls, *args, **kwargs):
      5         if cls not in cls._instances:
----> 6             cls._instances[cls] = super(MySingletonABCMeta, cls).__call__(*args)
      7         return cls._instances[cls]

TypeError: Can't instantiate abstract class MyAbstractSingletonClass with abstract methods my_abstract_method

In [53]:
class MyAbstractSingletonChild(MyAbstractSingletonClass):
    def __init__(self):
        pass
    
    def my_abstract_method(self):
        pass

In [54]:
a1 = MyAbstractSingletonChild()
b1 = MyAbstractSingletonChild()

In [55]:
type(a1), id(a1), type(b1), id(b1)


Out[55]:
(__main__.MyAbstractSingletonChild,
 4589319448,
 __main__.MyAbstractSingletonChild,
 4589319448)

Hashable Objects in Python

What items are hashable in Python?


In [60]:
a = [1,2]
{a:1}


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-60-78ccf1e710cf> in <module>()
      1 a = [1,2]
----> 2 {a:1}

TypeError: unhashable type: 'list'

In [61]:
print(a.__hash__)


None

In [ ]:
hash((1,2))

In [62]:
from collections import Hashable
class HashableObject(Hashable):
    __metaclass__ = ABCMeta

    def __init__(self):
        pass

    def __eq__(self, other):
        return True if isinstance(other, self.__class__) and self.get_key() == other.get_key() else False

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash(self.get_key())

    @abstractmethod
    def get_key(self):
        return self.__dict__.keys()

In [63]:
class MyHashableClass(HashableObject):
    def __init__(self, a,b,c):
        self.a = a
        self.b = b
        self.c = c
        
    def get_key(self):
        return self.a, self.b

In [64]:
a1 = MyHashableClass(1, 2, 3)
a2 = MyHashableClass(4, 5, 6)
a3 = MyHashableClass(4, 5, 8)

In [65]:
a1 == a2


Out[65]:
False

In [66]:
a2 == a3


Out[66]:
True

Comparable Objects in Python


In [67]:
class ComparableObject:
    def __init__(self):
        pass

    def __eq__(self, other):
        return True if isinstance(other, self.__class__) and self.__dict__ == other.__dict__ else False

    def __ne__(self, other):
        return not self == other

In [68]:
class MyComparableObject(ComparableObject):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

In [69]:
c1 = MyComparableObject(1, 2, 3)
c2 = MyComparableObject(4, 5, 6)
c3 = MyComparableObject(1, 2, 3)

In [70]:
c1 == c2


Out[70]:
False

In [71]:
c1 == c3


Out[71]:
True

Enum vs Polled Objects


In [72]:
from enum import Enum

In [73]:
class MyEnumeration(Enum):
    ONE = 'one'
    TWO = 'two'
    
    @staticmethod
    def get_enum(inp):
        for e in MyEnumeration:
            if e.value == inp:
                return e

In [74]:
a = MyEnumeration.get_enum('one')
a


Out[74]:
<MyEnumeration.ONE: 'one'>

Where Enums do not work and Solution


In [75]:
class MyBeanMeta(type):
    _instances = {}

    def __call__(cls, *args):
        print(args)
        key = tuple((cls, args))
        if key not in cls._instances:
            cls._instances[key] = super(MyBeanMeta, cls).__call__(*args)
        return cls._instances[key]

In [76]:
class MyBeanClass(metaclass=MyBeanMeta):
    def __init__(self, a ):
        self.a = a

In [77]:
bn1 = MyBeanClass(1)
bn2 = MyBeanClass(2)
bn3 = MyBeanClass(3)
bn4 = MyBeanClass(1)


(1,)
(2,)
(3,)
(1,)

In [78]:
id(bn1), id(bn2), id(bn3), id(bn4)


Out[78]:
(4581701504, 4581701616, 4588486160, 4581701504)

Use Generator wherever possible


In [ ]:
def with_generator():
    result = 0
    for x in get_data():
        result += x

def get_data():
    i = 0
    while i < 10000:
        yield i
        i += 1

In [ ]:
def without_generator():
    # print("Without Generator")
    result = 0
    for x in get_data_wo_gen():
        result += x

def get_data_wo_gen():
    ret_val = []
    for i in range(0, 10000):
        ret_val.append(i)

    return ret_val

Use native list comprehensions wherever possible


In [ ]:
a = []
for i in range(1000):
    a.append(i)

In [ ]:
a = [i for i in range(1000)]

In [ ]:
sum([i for i in range(1000)])

Use some of the performance optimized data structures

https://docs.python.org/3.6/library/collections.html